const std = @import("std");

pub fn buildIntervalMap() [18][14][]const u8 {
    const interval_table =
        \\ 0   1   b2  2   b3  3   4   #4  b5  5   b6  6   b7  7   
        \\ C   C   Db  D   Eb  E   F   F#  Gb  G   Ab  A   Bb  B   
        \\ C#  C#  D   D#  E   E#  F#  F## G   G#  A   A#  B   B#  
        \\ Db  Db  Ebb Eb  Fb  F   Gb  G   Abb Ab  Bbb Bb  Cb  C   
        \\ D   D   Eb  E   F   F#  G   G#  G#  A   Bb  B   C   C#  
        \\ D#  D#  E   E#  F#  F## G#  A   G## A#  B   B#  C#  C## 
        \\ Eb  Eb  Fb  F   Gb  G   Ab  A   Bbb Bb  Cb  C   Db  D   
        \\ E   E   F   F#  G   G#  A   A#  Bb  B   C   C#  D   D#  
        \\ F   F   Gb  G   Ab  A   Bb  B   Cb  C   Db  D   Eb  E   
        \\ F#  F#  G   G#  A   A#  B   B#  C   C#  D   D#  E   E#  
        \\ Gb  Gb  Abb Ab  Bbb Bb  Cb  C   Dbb Db  Ebb Eb  Fb  F   
        \\ G   G   Ab  A   Bb  B   C   C#  Db  D   Eb  E   F   F#  
        \\ G#  G#  A   A#  B   B#  C#  C## D   D#  E   E#  F#  F## 
        \\ Ab  Ab  Bbb Bb  Cb  C   Db  D   Ebb Eb  Fb  F   Gb  G   
        \\ A   A   Bb  B   C   C#  D   D#  Eb  E   F   F#  G   G#  
        \\ A#  A#  B   B#  C#  C## D#  D## E   E#  F#  F## G#  G## 
        \\ Bb  Bb  Cb  C   Db  D   Eb  E   Fb  F   Gb  G   Ab  A   
        \\ B   B   C   C#  D   D#  E   E#  F   F#  G   G#  A   A#  
    ;
    var m: [18][14][]const u8 = undefined;
    // split the words in the table, then build the interval map
    var lines = std.mem.splitSequence(u8, interval_table, "\n");
    var i: u8 = 0;
    while (lines.next()) |line| {
        var words = std.mem.tokenizeAny(u8, line, " \t");
        var j: u8 = 0;
        @setEvalBranchQuota(10000);
        while (words.next()) |word| {
            if (word.len > 0) {
                m[i][j] = word;
            }
            j += 1;
        }
        i += 1;
    }
    return m;
}

pub fn getNote(root_note: []const u8, interval: []const u8) ?[]const u8 {
    const interval_map: [18][14][]const u8 = comptime buildIntervalMap();
    // scan the first row, get the column id of the interval
    var interval_column: u8 = 0;
    for (interval_map[0], 0..) |column, i| {
        if (std.mem.eql(u8, interval, column)) {
            interval_column = @intCast(i);
            break;
        }
    }
    // if not found, return empty string
    if (interval_column == 0) {
        return null;
    }
    // get the row number of the root note
    var root_row: u8 = 0;
    for (interval_map, 0..) |row, i| {
        const note = row[0];
        if (std.mem.eql(u8, root_note, note)) {
            root_row = @intCast(i);
            break;
        }
    }
    if (root_row == 0) {
        return null;
    }
    return interval_map[root_row][interval_column];
}

pub fn main() !void {
    // the program takes two command line arguments: the root note and the interval
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();
    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);
    if (args.len != 3) {
        std.debug.print("Usage: {s} <root note> <interval>\n", .{args[0]});
        return;
    }
    const note = getNote(args[1], args[2]);
    if (note == null) {
        std.debug.print("Invalid interval\n", .{});
        return;
    }
    std.debug.print("{s} {s} = {s}\n", .{ args[1], args[2], note.? });
}

test "simple test" {
    try std.testing.expectEqualStrings("C", getNote("C", "1").?);
    try std.testing.expectEqualStrings("E#", getNote("C#", "3").?);
    // invalid root note
    try std.testing.expect(null == getNote("X", "3"));
    // invalid interval
    try std.testing.expect(null == getNote("C", "X"));
}
